// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: wsfuyibing <682805@qq.com>
// Date: 2024-07-10

package src

import (
	"context"
	"gitee.com/go-libs/runtime"
	"github.com/google/uuid"
	"sync"
	"sync/atomic"
	"time"
)

type (
	// Process is an interface used to manage provider that runtime like os
	// process.
	Process interface {
		// Add means adding multiple child processes to the current process.
		Add(children ...Process) Process

		// Del means deleting multiple child processes from the current
		// process.
		Del(children ...Process) Process

		// GetChild returns a child process with given name.
		GetChild(name string) (p Process, exists bool)

		// GetChildren return the child processes mapping.
		GetChildren() (children map[string]Process)

		// GetName return the process name.
		GetName() string

		// GetUid returns the process unique id.
		GetUid() string

		// Prevent to remove from parent when current process stopped.
		Prevent(prevent bool) Process

		// Restart send a restart signal to current process.
		Restart()

		// Start process with bio until stopped.
		Start(ctx context.Context) (err error)

		// Started return true if current process is started or stopping.
		Started() (yes bool)

		// Stop send a stop signal to current process.
		Stop()

		// Stopped return true if current process is stopped or not started.
		Stopped() bool

		// StopWait block process until stopped.
		StopWait()
	}

	process struct {
		// Follow fields are readonly, There are initialized when instance
		// created or before startup.
		name, uid string
		provider  Provider
		parent    Process

		childMapper  map[string]Process
		childRunning int32
		mu           *sync.Mutex

		cancel                     context.CancelFunc
		ctx                        context.Context
		restarting, stopping       bool
		started, quited, prevented bool
		startedTime                time.Time
	}
)

// New creates a new process instance.
func New(name string, provider Provider) Process {
	return (&process{
		childMapper: make(map[string]Process),
		mu:          new(sync.Mutex),
		name:        name, uid: uuid.NewString(),
		provider: provider,
	}).init()
}

func (o *process) Add(children ...Process) Process {
	adds := 0

	// Iterate given children then check it.
	for _, child := range children {
		// Skip nil child.
		if child == nil {
			continue
		}

		// The process with the same name is already in a child process.
		if p, exists := o.GetChild(child.GetName()); exists {
			// Send stop signal to stop running process.
			if p.Started() {
				p.Stop()
			}
		}

		// Update counter and bind parent process.
		adds++
		child.(*process).parent = o

		// Update child process mapping.
		o.mu.Lock()
		o.childMapper[child.GetName()] = child
		o.mu.Unlock()
	}

	// If adding child processes on a running process, start them immediately
	// after adding them.
	if adds > 0 {
		o.mu.Lock()
		ctx := o.ctx
		o.mu.Unlock()

		// Start child processes if current process context is existed and not
		// cancelled.
		if ctx != nil && ctx.Err() == nil {
			o.startChildren(ctx)
		}
	}
	return o
}

func (o *process) Del(children ...Process) Process {
	// Iterate given children then compare with current process.
	for _, child := range children {
		// Skip nil child.
		if child == nil {
			continue
		}

		// Child process found in children.
		if p, exists := o.GetChild(child.GetName()); exists {
			// Important:
			// For example, if process A and process B have the same name,
			// process A may have been removed before exiting, and subsequent
			// deletion should be ignored. Otherwise, process B may be removed.
			if p.GetUid() != child.GetUid() {
				continue
			}

			// Send stop signal if running.
			if p.Started() {
				p.Stop()
			}

			// Remove from children.
			o.mu.Lock()
			delete(o.childMapper, child.GetName())
			o.mu.Unlock()
		}
	}
	return o
}

func (o *process) GetChild(name string) (p Process, exists bool) {
	o.mu.Lock()
	p, exists = o.childMapper[name]
	o.mu.Unlock()
	return
}

func (o *process) GetChildren() (children map[string]Process) {
	o.mu.Lock()
	children = o.childMapper
	o.mu.Unlock()
	return
}

func (o *process) GetName() string {
	return o.name
}

func (o *process) GetUid() string {
	return o.uid
}

func (o *process) Prevent(prevented bool) Process {
	o.mu.Lock()
	o.prevented = prevented
	o.mu.Unlock()
	return o
}

func (o *process) Restart() {
	o.mu.Lock()
	defer o.mu.Unlock()

	if o.ctx != nil && o.ctx.Err() == nil {
		o.quited, o.restarting = false, true
		o.cancel()
	}
}

func (o *process) Start(ctx context.Context) (err error) {
	// Ignore nil context.
	if ctx == nil {
		err = ErrNilContext
		return
	}

	// Ignore nil provider.
	if o.provider == nil {
		err = ErrNilProvider
		return
	}

	// Lock process state.
	o.mu.Lock()

	// Return if the process state in starting or stopping.
	if o.stopping || o.restarting {
		o.mu.Unlock()
		return
	}

	// Return if started already.
	if o.started {
		o.mu.Unlock()
		err = ErrStartedAlready
		return
	}

	// Update process state to avoid concurrency operations.
	o.started, o.quited, o.restarting, o.stopping = true, false, false, false
	o.startedTime = time.Now()
	o.mu.Unlock()
	runtime.GetCounter().ProcessCounter().Add(o.uid, o.name, o.startedTime)

	// Clean state when stopped.
	defer func() {
		runtime.GetCounter().ProcessCounter().Del(o.uid)

		// Revert process state.
		o.mu.Lock()
		parent, prevented := o.parent, o.prevented
		o.mu.Unlock()

		// Notify the parent process to delete the current process.
		if parent != nil && !prevented {
			parent.Del(o)
		}

		// Revert process state as initial.
		o.mu.Lock()
		o.ctx, o.cancel, o.parent = nil, nil, nil
		o.started, o.quited = false, true
		o.mu.Unlock()
	}()

	// Block until current or parent process stopped.
	for {
		// Return for parent context cancelled.
		if ctx.Err() != nil {
			return
		}

		// Return for stop signal.
		if o.quited {
			return
		}

		// Build process context.
		o.mu.Lock()
		o.ctx, o.cancel = context.WithCancel(ctx)
		o.quited = true
		o.mu.Unlock()

		// Invoke before hook of the provider, Return if error returned.
		if handler, ok := o.provider.(ProviderBefore); ok {
			if err = o.handle(o.ctx, handler.Before); err != nil {
				return
			}
		}

		// Invoke child processes startup.
		o.startChildren(o.ctx)

		// Invoke run hook of the provider.
		err = o.handle(o.ctx, o.provider.Run)

		// Cancel current process context when provider.Run() hook ended. It's
		// used to stop children processes.
		if o.ctx.Err() == nil {
			o.cancel()
		}

		// Block and wait all children are stopped.
		o.waitChildrenStopped()

		// Invoke after hook of the provider if no error occurred on run hook.
		if err == nil {
			if handler, ok := o.provider.(ProviderAfter); ok {
				err = o.handle(o.ctx, handler.After)
			}
		}
	}
}

func (o *process) Started() (started bool) {
	o.mu.Lock()
	started = o.started
	o.mu.Unlock()
	return
}

func (o *process) Stop() {
	o.mu.Lock()
	defer o.mu.Unlock()

	if o.ctx != nil && o.ctx.Err() == nil {
		o.quited, o.stopping = true, true
		o.cancel()
	}
}

func (o *process) Stopped() (stopped bool) {
	stopped = !o.Started()
	return
}

func (o *process) StopWait() {
	// Send stop signal to process.
	o.Stop()

	// Block until process stopped
	for {
		if o.Stopped() {
			return
		}
		time.Sleep(time.Millisecond * 3)
	}
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

func (o *process) handle(ctx context.Context, handler func(context.Context) error) (err error) {
	// Invoked when handler ended.
	defer func() {
		// Recover and catch runtime panic.
		if r := recover(); r != nil {
			runtime.GetCounter().ProcessCounter().IncrFatalCount()

			// Set error reason if not specified by handler.
			if err == nil {
				err = ErrHookPanic
			}

			// Invoke panic.
			o.handlePanic(ctx, r)
		}
	}()

	// Invoke handler.
	err = handler(ctx)
	return
}

func (o *process) handlePanic(ctx context.Context, v any) {
	// Discard runtime panic.
	defer func() {
		if r := recover(); r != nil {
			runtime.GetCounter().ProcessCounter().IncrFatalCount()
		}
	}()

	// Invoke handler.
	if hook, ok := o.provider.(ProviderPanic); ok {
		hook.Panic(ctx, v)
	}
	return
}

func (o *process) init() *process {
	runtime.GetCounter().ProcessCounter().IncrCreatedCount()
	return o
}

func (o *process) startChildren(ctx context.Context) {
	o.mu.Lock()
	defer o.mu.Unlock()

	// Iterate all children.
	for _, c := range o.childMapper {
		// Skip started process.
		if c.Started() {
			continue
		}

		// Start child process in a goroutine.
		atomic.AddInt32(&o.childRunning, 1)
		go func(child Process) {
			defer atomic.AddInt32(&o.childRunning, -1)
			_ = child.Start(ctx)
		}(c)
	}
}

func (o *process) waitChildrenStopped() {
	for {
		// All child processes are stopped.
		if atomic.LoadInt32(&o.childRunning) == 0 {
			break
		}

		// Sleep for a while and recheck.
		time.Sleep(time.Millisecond * 10)
	}
}
